﻿//-----------------------------------------------------------------------
// <copyright file="SceneManager.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace TileTool.Base.Scene
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using TileTool.Common;
    using TileTool.Message;
    using TileTool.Render;
    using TileTool.Scene;
    using TileTool.Scene.Collision;
    using TileTool.Scene.Entity;
    using TileTool.Scene.Entity.Group;
    using TileTool.Scene.Message;

    /// <summary>
    /// Base implementation of the scene.
    /// </summary>
    public class SceneManager : ISceneManager
    {
        #region Fields - Private
        
        /// <summary>
        /// Message router we will send messages on.
        /// </summary>
        private MessageRouter msgRouter;

        /// <summary>
        /// Currently selected entities.
        /// </summary>
        private IEntityCollection selection = new EntityCollection();

        /// <summary>
        /// Collection of all entities in the scene.
        /// </summary>
        private IEntityCollection allEntities = new EntityCollection();

        #endregion

        #region Constructor

        /// <summary>
        /// Construct a scene from create data.
        /// </summary>
        /// <param name="createData">Scene creation data.</param>
        public SceneManager(SceneManagerCreateData createData)
        {
            CreateData.Validate(createData);

            this.msgRouter = createData.MessageRouter;
        }

        #endregion

        #region Events

        /// <summary>
        /// Event triggered when an entity changed.
        /// </summary>
        public event EntityChanged EntityChanged;

        #endregion

        #region Properties

        /// <summary>
        /// Gets the current selection as a read-only collection.
        /// </summary>
        public IEntityCollection Selection
        {
            get
            {
                return new ReadOnlyEntityCollection(this.selection);
            }
        }

        /// <summary>
        /// Gets all entities in the scene as a read-only collection.
        /// </summary>
        public IEntityCollection AllEntities
        {
            get
            {
                return new ReadOnlyEntityCollection(this.allEntities);
            }
        }

        #endregion

        #region Methods - Public - Entity management

        /// <summary>
        /// Clear the scene and remove all entities from it. Must send a ISceneClearedMessage
        /// after a successful call.
        /// </summary>
        public void Clear()
        {
            // First, clear the selection.
            this.SetSelection(new EntityCollection());

            // Clear all entities out of our list.
            this.allEntities.Clear();

            SceneClearedMessage msg = new SceneClearedMessage();

            this.SendMessage(msg);
        }

        /// <summary>
        /// Add the passed entities to the scene. Must send an ISceneEntityAddedMessage message
        /// on a successful add.
        /// </summary>
        /// <param name="ents">Entities to add to the scene.</param>
        public void AddEntities(IEntityCollection ents)
        {
            if (ents == null)
            {
                throw new ArgumentNullException("ents", "Entity collection must be non-null.");
            }

            if (ents.Count == 0)
            {
                return;
            }

            // Keep a list of the entities that we actually added
            // because the input list may contain entities that
            // are already in the scene.
            EntityCollection addedEnts = new EntityCollection();

            foreach (IEntity ent in ents)
            {
                if (this.allEntities.Contains(ent))
                {
                    continue;
                }

                if (ent.Scene != null)
                {
                    continue;
                }

                addedEnts.Add(ent);
                this.AddEntityToScene(ent);
            }

            SceneEntityAddedMessage addMsg = new SceneEntityAddedMessage();

            addMsg.Entities = addedEnts.AsReadOnly();

            this.SendMessage(addMsg);
        }

        /// <summary>
        /// Remove the passed entities from the scene. Must send a ISceneEntityRemovedMessage message
        /// on a successful remove.
        /// </summary>
        /// <param name="ents">Entities to remove from the scene.</param>
        public void RemoveEntities(IEntityCollection ents)
        {
            if (ents == null)
            {
                throw new ArgumentNullException("ents", "Entity collection must be non-null.");
            }

            if (ents.Count == 0)
            {
                return;
            }

            // Keep a list of the entities that we actually removed
            // because the input list may contain entities that
            // are not in the scene.
            EntityCollection removedEnts = new EntityCollection();

            foreach (IEntity ent in ents)
            {
                if (!this.allEntities.Contains(ent))
                {
                    return;
                }

                this.RemoveEntityFromScene(ent);
                removedEnts.Add(ent);
            }

            this.UpdateSelectionForRemove(removedEnts);

            SceneEntityRemovedMessage removeMsg = new SceneEntityRemovedMessage();

            removeMsg.Entities = removedEnts.AsReadOnly();

            this.SendMessage(removeMsg);
        }

        /// <summary>
        /// Set the selection to the passed collection of entities.
        /// 
        /// A copy of the passed entity collection will be made.
        /// 
        /// A ISceneSelectionChangedMessage must be sent on a successful selection change.
        /// </summary>
        /// <param name="newSelection">Collection of entities to set the selection to.</param>
        public void SetSelection(IEntityCollection newSelection)
        {
            // The first thing we do is remove selection from all objects that are
            // currently selected
            foreach (IEntity ent in this.selection)
            {
                ent.Selected = false;
            }

            // Change the selection.
            IEntityCollection oldSelection = this.selection;

            if (newSelection == null)
            {
                newSelection = new EntityCollection();
            }

            this.selection = newSelection.Clone();

            // Mark all items from the new selection as selected.
            foreach (IEntity ent in this.selection)
            {
                ent.Selected = true;
            }

            // Send out the selection changed message. Always clone the collections
            // so that the message has a snapshot at this time.
            SceneSelectionChangedMessage msg = new SceneSelectionChangedMessage();

            msg.Selection = this.selection.Clone().AsReadOnly();
            msg.OldSelection = oldSelection.Clone().AsReadOnly();

            this.SendMessage(msg);
        }

        #endregion

        #region Methods - Public - Render

        /// <summary>
        /// Draw the entities in the scene with the passed renderer.
        /// </summary>
        /// <param name="renderData">Rendering data. Always non-null.</param>
        public void Draw(IRenderData renderData)
        {
            ICanvasRenderer renderer = renderData.Renderer;

            foreach (IEntity entity in this.allEntities)
            {
                if (!entity.Visible)
                {
                    continue;
                }

                renderer.PushMatrix(entity.Matrix);
                entity.Draw(renderData);
                renderer.PopMatrix();
            }

            foreach (IEntity entity in this.selection)
            {
                renderer.PushMatrix(entity.Matrix);
                entity.DrawSelected(renderData);
                renderer.PopMatrix();
            }
        }

        #endregion

        #region Methods - Public Collision

        /// <summary>
        /// Handle a collision request.
        /// </summary>
        /// <param name="req">Collision request.</param>
        /// <returns>Returns a collision result object. Will always be non-null.</returns>
        public CollisionResult HandleCollision(CollisionRequest req)
        {
            if (req == null)
            {
                throw new ArgumentNullException("req", "Request must be non-null.");
            }

            CollisionResult res = new CollisionResult();
            ISet<IEntity> ignoreSet = req.IgnoreSet;
            
            foreach (IEntity entity in this.allEntities)
            {
                if (ignoreSet.Contains(entity))
                {
                    continue;
                }

                if (!entity.Visible)
                {
                    continue;
                }

                IEntity hitEntity = entity.HandleCollision(req);
                if (hitEntity == null)
                {
                    continue;
                }

                hitEntity = this.HandleGroupCollisionFilter(hitEntity);

                res.HitEntities.Add(hitEntity);
            }

            return res;
        }

        #endregion

        #region Methods - Private

        /// <summary>
        /// Add an entity to the scene.
        /// </summary>
        /// <param name="entity">Entity to add to the scene.</param>
        private void AddEntityToScene(IEntity entity)
        {
            Debug.Assert(entity != null, "Entity must be non-null.");

            entity.Selected = false;
            entity.Frozen = false;
            entity.Visible = true;
            entity.Scene = this;

            entity.Changed += this.HandleEntityChanged;

            this.allEntities.Add(entity);
        }

        /// <summary>
        /// Remove the passed entity from the scene.
        /// </summary>
        /// <param name="entity">Entity to remove from the scene.</param>
        private void RemoveEntityFromScene(IEntity entity)
        {
            Debug.Assert(entity != null, "Entity must be non-null.");

            entity.Selected = false;
            entity.Frozen = false;
            entity.Visible = false;
            entity.Scene = null;

            entity.Changed -= this.HandleEntityChanged;

            this.allEntities.Remove(entity);
        }

        /// <summary>
        /// Update the selected objects collection as needed based on the removed objects.
        /// </summary>
        /// <param name="removed">Collection of objects that were removed.</param>
        private void UpdateSelectionForRemove(IEntityCollection removed)
        {
            if (removed.Count == 0)
            {
                return;
            }

            bool selectionChanged = false;
            foreach (IEntity ent in removed)
            {
                if (this.selection.Contains(ent))
                {
                    selectionChanged = true;
                    break;
                }
            }

            if (!selectionChanged)
            {
                return;
            }

            IEntityCollection newSelection = this.selection.Clone();

            foreach (IEntity ent in removed)
            {
                if (newSelection.Contains(ent))
                {
                    newSelection.Remove(ent);
                }
            }

            this.SetSelection(newSelection);
        }

        /// <summary>
        /// Utility function to send a message and handle setting the scene
        /// reference in the message for scene messages.
        /// </summary>
        /// <param name="msg">Message to send.</param>
        private void SendMessage(MessageBase msg)
        {
            if (msg as SceneMessage != null)
            {
                SceneMessage sceneMessage = (SceneMessage)msg;

                sceneMessage.Scene = this;
            }

            this.msgRouter.SendMessage(msg);
        }

        /// <summary>
        /// Handle an entity changing.
        /// </summary>
        /// <param name="args">Entity change event args</param>
        private void HandleEntityChanged(EntityChangedEventArgs args)
        {
            EntityChanged changed = this.EntityChanged;

            if (changed != null)
            {
                changed(args);
            }
        }

        #endregion

        #region Methods - Private - Collision

        /// <summary>
        /// Filter a entity that we hit to see if it is part of a group.
        /// 
        /// If the entity is part of a group, return the group that it is a
        /// part of.
        /// 
        /// Will always return the group that is farthest up the parent chain.
        /// </summary>
        /// <param name="hitEntity">Entity that was hit.</param>
        /// <returns>Returns the filtered collision entity.</returns>
        private IEntity HandleGroupCollisionFilter(IEntity hitEntity)
        {
            if (hitEntity.Parent == null)
            {
                return hitEntity;
            }

            IEntity current = hitEntity.Parent;
            while (current != null)
            {
                GroupEntity groupEntity = current as GroupEntity;
                
                // If we hit a group entity, our new hit entity becomes
                // the group entity.
                if (groupEntity != null)
                {
                    // If the group is open, we stop searching since to
                    // get to the open group we have already picked the
                    // best closed group to select.
                    if (groupEntity.Open)
                    {
                        break;
                    }
                    
                    hitEntity = groupEntity;
                }

                current = current.Parent;
            }

            return hitEntity;
        }

        #endregion
    }
}
